/*

  $Id: message.js 178 2008-06-05 13:48:28Z altblue $
 
  Message: Notification widgets

  (c) 2006 Marius Feraru <altblue@n0i.net>

  This module is free software; you can redistribute it
  and/or modify it under the same terms as Perl itself.
*/

var Message = {
  /* message constructors */
  add      : function(m,o) {return new Message.Basic(m,o)},
  error    : function(m,o) {return new Message.Error(m,o)},
  warning  : function(m,o) {return new Message.Warning(m,o)},
  info     : function(m,o) {return new Message.Info(m,o)},
  mail     : function(m,o) {return new Message.Mail(m,o)},
  choose   : function(c,o) {return new Message.Select(c,o)},
  confirm  : function(o)   {return new Message.Confirm(o)},
  progress : function(o)   {return new Message.Progress(o)},
  prompt   : function(o)   {return new Message.Prompt(o)},

  /* default options for all messages */
  defaultOptions: {
      debug:      '',
      id:         '',
      level:      'info',
      timeout:    3,
      noClose:    false,
      beforeShow: function (m) {},
      onShow:     function (m) {},
      onClose:    function (m) {},
      remove:     false
  },

  /* MessageArea tools: show or not? */
  tools: false,
  /* MessageArea animation: use or not? */
  animation: true,
  /* messages counter - used to automatically generate Message IDs */
  counter: 0,

  /* #MessageArea element */
  mw: '',
  /* Messages wrapper */
  ma: '',
  /* toolbar */
  tb: '',
  /* "show all" button */
  sa: '',
  /* "remove all" button */
  ra: '',
  /* "hide all" button */
  ha: '',

  /* MessageArea initializer: to be loaded on Page Load */
  initialize: function () {
    if (this.ma) return false; // already done?

    /* already have a #MessageArea? move it to #MessageAreaOld */
    var oma = $('MessageArea');
    if (oma) oma.setAttribute('id', 'MessageAreaOld');

    /* setup MessageArea */
    this.mw = $T('div', document.body, document.body.firstChild, {id: 'MessageArea'});

    /* setup Messages Container */
    this.ma = $T('div', this.mw, {id: 'MessageAreaWrapper'});

    /* ShowALL button */
    this.sa = $T('a', { id:'MessageShowAll', href:'#', title: 'show all messages'})
      .update('sho'.split('').join('<br />') + '<br /><span class="accesskey">w</span>');
    this.sa.hide();
    document.body.insertBefore(this.sa, this.mw);
    this.sa.setAttribute('accesskey', 'w');
    this.saListener = this.showAll.bindAsEventListener(this);
    this.sa.observe('click', this.saListener);

    /* setup Toolbar */
    this.tb = $T('div', this.mw, {id: 'MessageAreaToolBar'});
    this.tb.hide();

    /* RemoveALL button */
    this.ra = $T('a', this.tb, {
      href: '#',
      id: 'MessageRemoveAll',
      title: 'remove all messages'
    }).update('remove all');
    this.raListener = this.removeAll.bindAsEventListener(this);
    this.ra.observe('click', this.raListener);

    /* HideALL button */
    this.ha = $T('a', this.tb, {
      href: '#',
      id: 'MessageHideAll',
      title: 'hide all messages'
    }).update('hid<span class="accesskey">e</span> all');
    this.ha.setAttribute('accesskey', 'e');
    this.haListener = this.hideAll.bindAsEventListener(this);
    this.ha.observe('click', this.haListener);

    /* Populate MessageArea with preloaded messages */
    if (oma) this.loadOldMessages(oma);
  },

  /* Twist messages already added statically in HTML */
  loadOldMessages: function (oma) {
    var divs = oma.getElementsBySelector('div.message');
    for (var i = 0, len = divs.length; i < len; i++) {
      var opt = {level: divs[i].className};
      if (i === len - 1) {
        opt.onShow = this.refreshMessageArea.bind(this);
      }
      new Message.Basic(divs[i].innerHTML, opt);
    }
    oma.remove();
  },

  setToolsStatus: function (enabled) {
    this.tools = enabled;
    if (enabled) {
      this.defaultOptions.remove  = false;
      this.defaultOptions.timeout = 3;
    }
    else {
      this.defaultOptions.remove = true;
      this.defaultOptions.timeout = 5;
    }
    if (!this.ma) return;
    if (enabled) {
      this.refreshMessageArea();
    } else {
      this.tb.hide();
      this.sa.hide();
    }
  },

  setAnimationStatus: function (enabled) {
    this.animation = enabled;
  },

  /* return messages */
  messages: function(filter) {
    var kids = this.ma.getElementsBySelector('div.message');
    if (!filter) return kids;
    return kids.findAll(filter);
  },

  /* lazy shortcuts */
  visibleMessages: function() {
    return this.messages(Element.visible);
  },
  hiddenMessages: function() {
    return this.messages(Element.hidden);
  },

  /* Hide ALL Messages in MessageArea */
  hideAll: function(ev) {
    Event.stop(ev);
    var messages = this.messages();
    if (this.tools) {
      this.tb.hide();
      if (messages.length) this.sa.show();
    }
    if (!messages.length) return;
    if (this.animation) {
      new Effect.SlideUp(this.ma, {afterFinish: function() {
          messages.invoke('hide');
          this.ma.show();
        }.bind(this)
      });
    } else {
      this.ma.hide();
      messages.invoke('hide');
      this.ma.show();
    }
  },

  /* Show ALL Messages in MessageArea */
  showAll: function(ev) {
    Event.stop(ev);
    var messages = this.messages();
    if (!messages.length) return;

    var finalize = function (msg) {
      if (this.tools) {
        this.sa.hide();
        this.tb.show();
      }
      this.focusMessage(msg);
    }.bind(this, messages[0]);

    if (this.animation) {
      this.ma.hide();
    }
    messages.invoke('show');
    if (this.animation) {
      new Effect.SlideDown(this.ma, {afterFinish: finalize});
    } else {
      finalize();
    }
  },

  /* Remove ALL Messages from MessageArea */
  removeAll: function(ev) {
    Event.stop(ev);
    this.mw.hide();
    if (this.tools) {
      this.tb.hide();
      this.sa.hide();
    }
    this.messages().invoke('remove');
    this.mw.show();
  },

  /* focus first message, repaint buttons, etc */
  refreshMessageArea: function() {
    var visible = this.visibleMessages(), hidden  = this.hiddenMessages();
    if (visible.length) this.focusMessage(visible[0]);
    if (this.tools) {
      this.sa[ hidden.length ? 'show' : 'hide' ]();
      if (hidden.length)
        this.tb[ visible.length ? 'show' : 'hide' ]();
    }
  },

  /* "focus" a message somehow ;-) */
  focusMessage: function(msg) {
    var toFocus = msg.getElementsByTagName('select')[0]
      || msg.getElementsByTagName('textarea')[0]
      || msg.getElementsByTagName('input')[0]
      || msg.getElementsByTagName('button')[0]
      || msg.getElementsByTagName('a')[0];
    if (!toFocus) return;
    toFocus = $(toFocus); // for IE
    if (Message.animation && !toFocus.hasClassName('close'))
      new Effect.Highlight( toFocus, {duration: 0.3, startcolor: '#99ccff',
        restorecolor: toFocus.getStyle('background-color')});
    try { // browsers complain if control it's hidden
      toFocus.focus();
    } catch (e) {};
  }

};


Message.Base = function() {};
Message.Base.prototype = {
  setOptions: function(o) {
    this.options = {};
    Object.extend(this.options, Message.defaultOptions);
    Object.extend(this.options, o || {});
    if (!this.options.id) this.options.id = Message.counter++;
  },

  create: function(m) {
    this.msg = $T('div', {className:'message ' + this.options.level, id:this.options.id});
    this.msg.hide();

    if (typeof m == 'string') {
      this.msg.innerHTML = m;
    } else {
      try {
        this.msg.appendChild(m);
      } catch (e) {
        this.msg.innerHTML = window.JSON ? JSON.stringify(m) : Object.inspect(m);
      };
    }

    if (this.options.debug) {
      $T('h4', this.msg, {className: 'debug'}).update('Debug');
      $T('div', this.msg, {className: 'debug'}).update(this.options.debug);
    }

    if (!this.options.noClose) this._addCloseButton();

    var ma = Message.ma;
    ma.insertBefore(this.msg, ma.firstChild);

    this.beforeShow();
    if (Message.animation) {
      new Effect.Appear(this.msg, {duration: 0.2,
        afterFinish: this.onShow.bind(this)});
    } else {
      this.msg.show();
      this.onShow();
    }
    return this;
  },

  destroy: function() {
    this.options.remove = true;
    this.close();
  },

  close: function(ev) {
    if (ev) try { Event.stop(ev) } catch(e) {};
    this.beforeClose();
    if (Message.animation) {
      new Effect.Fade(this.msg, {duration: 0.2,
        afterFinish: this.onClose.bind(this)});
    } else {
      this.msg.hide();
      this.onClose();
    }
  },

  beforeShow: function () {
    if (typeof this._beforeShow == 'function')
      this._beforeShow();
    if (typeof this.options.beforeShow == 'function')
      this.options.beforeShow(this.msg);
  },

  onShow: function () {
    Message.focusMessage(this.msg);
    if (typeof this._onShow == 'function')
      this._onShow();
    if (typeof this.options.onShow == 'function')
      this.options.onShow(this.msg);
    if (this.options.timeout > 0)
      setTimeout(this.close.bind(this), this.options.timeout * 1000);
  },

  beforeClose: function () {
    if (typeof this._beforeClose == 'function')
      this._beforeClose();
    if (typeof this.options.beforeClose == 'function')
      this.options.beforeClose(this.msg);
  },

  onClose: function () {
    if (typeof this._onClose == 'function')
      this._onClose();
    if (typeof this.options.onClose == 'function')
      this.options.onClose(this.msg);
    try {
      this.msg[this.options.remove ? 'remove' : 'hide']();
    } catch (e) {};
    Message.refreshMessageArea();
  },

  _addCloseButton: function() {
    if (Message.tools)
      this.msg.observe('click', this._clickHandler.bindAsEventListener(this));
    var a = $T('a', this.msg, {className: 'close', href: '#',
      title: 'hide this message'}).update('×');
    a.observe('click', this.close.bindAsEventListener(this));
    a.observe('keypress', this._keyHandler.bindAsEventListener(this));
    return a;
  },

  _keyHandler: function(ev) {
    if (ev.keyCode == Event.KEY_ESC) {
      this.close(ev);
      return false;
    }
    return true;
  },

  _clickHandler: function(ev) {
    ev || (ev = window.event);
    var el = Event.element(ev);
    if ( el == this.msg || ( ( el.tagName
      && !/^(?:a|input|select|option|button|label|textarea)$/.test(el.tagName.lc())
      ) && typeof el.onclick != 'function'
    )) {
      Event.stop(ev);
      this.close();
      return false;
    }
  },

  _monitorFocus: function(element) {
    element.hasFocus = false;
    element.observe('focus', function(){this.hasFocus = true }.bind(element));
    element.observe('blur',  function(){this.hasFocus = false}.bind(element));
  }

};


Message.Basic = Class.create();
Message.Basic.prototype = Object.extend(new Message.Base(), {
  initialize: function(m, o) {
    Message.initialize();
    this.setOptions(o);
    return this.create(m);
  }
});


Message.Error = Class.create();
Message.Error.prototype = Object.extend(new Message.Base(), {
  initialize: function(m, o) {
    Message.initialize();
    this.setOptions(Object.extend({level: 'error'}, o || {}));
    return this.create(m);
  }
});


Message.Warning = Class.create();
Message.Warning.prototype = Object.extend(new Message.Base(), {
  initialize: function(m, o) {
    Message.initialize();
    this.setOptions(Object.extend({level: 'warning'}, o || {}));
    return this.create(m);
  }
});


Message.Info = Class.create();
Message.Info.prototype = Object.extend(new Message.Base(), {
  initialize: function(m, o) {
    Message.initialize();
    this.setOptions(Object.extend({level: 'info'}, o || {}));
    return this.create(m);
  }
});


Message.Mail = Class.create();
Message.Mail.prototype = Object.extend(new Message.Base(), {
  initialize: function(m, o) {
    Message.initialize();
    this.setOptions(Object.extend({level: 'mail'}, o || {}));
    return this.create(m);
  }
});


Message.Prompt = Class.create();
Message.Prompt.prototype = Object.extend(new Message.Base(), {
  initialize: function(o) {
    Message.initialize();
    this.setOptions(Object.extend({
        level:   'prompt',
        noClose: true,
        handler: function (value) {},
        label:   'Input value:',
        value:   '',
        rows:    1,
        remove:  false,
        timeout: 0
      }, o || {}));
    this.createForm();
    return this.create(this.form);
  },

  createForm: function() {
    this.form  = $T('form', {method: 'get', id: 'form_' + this.options.id,
      action: '#'});
    this.form.observe('submit', this._onSubmit.bindAsEventListener(this));
    this.label = $T('label', this.form, {'for': 'f_' + this.options.id})
      .update(this.options.label);
    if (this.options.rows < 2) {
      this.form.appendChild(Element.textNode('  '));
      this.input = $T('input', this.form, {type: 'text', autocomplete: 'off',
        value: this.options.value, className: 'text', id: 'f_' + this.options.id });
    } else {
      $T('br', this.form);
      this.input = $T('textarea', this.form, {rows: this.options.rows, cols: 40,
        className: 'text', id: 'f_' + this.options.id}).update(this.options.value);
    }
    this.input.setStyle({'background-color': '#fff', 'color': '#000'});
    this.input.observe('keypress', this._keyHandler.bindAsEventListener(this));
    this.form.appendChild(Element.textNode('  '));
    this.submit = $T('input', this.form, {type: 'submit', className: 'text submit',
      value: ' enter '});
  },

  _beforeShow: function() {
    this.msg.show();
    var dF = this.msg.getDimensions();
    var dI = this.input.getDimensions();
    var oI = Position.cumulativeOffset(this.input);
    var dS = this.submit.getDimensions();
    var oS = Position.cumulativeOffset(this.submit);
    /* 64 = close-button + possible scroll */
    this.input.style.width
      = parseInt((dF.width - oI[0]) - (oS[0] - oI[0] - dI.width) - dS.width - 64) + 'px';
    this.msg.hide();
  },

  _onSubmit: function(ev) {
    if (ev) Event.stop(ev);
    this.options.handler( $F(this.input) );
    this.destroy();
  }
});


Message.Confirm = Class.create();
Message.Confirm.prototype = Object.extend(new Message.Base(), {
  initialize: function(options) {
    Message.initialize();
    this.setOptions(Object.extend({
        handler:  function (value) {},
        noClose:  true,
        focusYes: false,
        elements: [],
        confirmClass: 'toBeConfirmed',
        label:    'Are you sure?',
        labelYes: 'Yes',
        labelNo:  'No',
        level:    'confirm',
        remove:   true,
        timeout:  0
      }, options || {}));
    this.createForm();
    return this.create(this.form);
  },

  createForm: function() {
    this.form  = $T('form', {method: 'get', id: 'form_' + this.options.id,
      action: '#'});
    this.form.observe('keypress', this._onKeyPress.bindAsEventListener(this));

    this.label = $T('label', this.form).update(this.options.label);

    this.form.appendChild($TT('   '));

    this.NO = $T('button', this.form, {id: 'f_' + this.options.id + '_no',
      value: 0, className: 'submit text cancel'}).update(' ' + this.options.labelNo + ' ');
    this.NO.setAttribute('accesskey', 'n');
    this.NO.observe('click', this._onSubmit.bindAsEventListener(this,false));
    this._monitorFocus(this.NO);

    this.form.appendChild($TT('   '));

    this.YES = $T('button', this.form, {id: 'f_' + this.options.id + '_yes',
      value: 1, className: 'submit text commit'}).update(' ' + this.options.labelYes + ' ');
    this.YES.setAttribute('accesskey', 'y');
    this.YES.observe('click', this._onSubmit.bindAsEventListener(this,true));
    this._monitorFocus(this.YES);
  },

  _onKeyPress: function(ev) {
    if (ev.keyCode == Event.KEY_ESC) {
      this.close(ev);
      return false;
    }
    if (ev.keyCode == Event.KEY_LEFT || ev.keyCode == Event.KEY_RIGHT) {
      this[this.NO.hasFocus ? 'YES' : 'NO'].focus();
      return true;
    }
    var cod = ev.charCode || ev.keyCode;
    var chr = String.fromCharCode(cod).uc();
    if (chr == 'Y') {
      this._onSubmit(ev, true);
    } else if (chr == 'N') {
      this._onSubmit(ev, false);
    }
    return true;
  },

  _onSubmit: function(ev, confirm) {
    if (ev) Event.stop(ev);
    if (!confirm && this.options.elements.length > 0) {
      this.options.elements.invoke('removeClassName', this.options.confirmClass);
    }
    this.options.handler(confirm, this.options.elements);
    this.destroy();
  },

  _onShow: function() {
    if (this.options.elements && this.options.elements.length > 0)
      this.options.elements.invoke('addClassName', this.options.confirmClass);
    this[this.options.focusYes ? 'YES' : 'NO'].focus();
  }
});


Message.Select = Class.create();
Message.Select.prototype = Object.extend(new Message.Base(), {
  initialize: function(choices, options) {
    Message.initialize();
    this.setOptions(Object.extend({
        level: 'select',
        label: 'Select:',
        type:  'select',
        handler: function (value) {},
        noClose:  true,
        value:   '',
        remove:  false,
        timeout: 0
      }, options || {}));
    this.choices = choices;
    this.options.buttons = this.options.type == 'buttons';
    this.options.links   = this.options.type == 'links';
    this.options.select  = !this.options.links && !this.options.buttons;
    if ( this.options.links && (!options || !options.handler) ) {
      this.options.handler = function (value) {
        setTimeout(function(){window.location = this}.bind(value), 10);
      };
    }
    this.createForm();
    return this.create(this.form);
  },

  createForm: function() {
    this.form  = $T('form', {method: 'get', id: 'form_' + this.options.id,
      action: '#'});
    this.form.observe('submit', this._onSubmit.bindAsEventListener(this));
    this.label = $T('label', this.form).update(this.options.label);
    if (this.options.select) {
      this.label.setAttribute('for', 'f_' + this.options.id);
    }
    this.form.appendChild(Element.textNode('   '));
    if (this.options.select) {
      this.input = $T('select', this.form, {className: 'text', id: 'f_' + this.options.id})
        .setStyle({'background-color': '#fff', 'color': '#000'})
        .observe('keypress', this._keySelectHandler.bindAsEventListener(this));
    } else {
      this.form.observe('keypress', this._keyListHandler.bindAsEventListener(this));
    }

    for (var idx = 0, len = this.choices.length; idx < len; idx++) {
      var choice = this.choices[idx], label, value;
      if (typeof choice === 'string') {
        if (this.options.links) {
          value = choice;
          var url = parseURL(choice);
          label = (url.host || choice).replace(/^www\d*\./, '');
          if (!url.path || url.path == '/') {
            label = label.replace(/\.(com|net|org|ro|co.uk)$/, '');
          } else {
            label += url.path;
          }
        } else {
          label = value = choice;
        }
      } else if (typeof choice == 'object' && choice.constructor == Array) {
        label = choice[0], value = choice[1];
      } else {
        label = choice.label || choice.name, value = choice.value;
      };

      var option = this.options.select ? $T('option', this.input)
        : $T(this.options.buttons ? 'button' : 'a', this.form);

      option.innerHTML = ' ' + label + ' ';
      option[this.options.links ? 'href' : 'value'] = value;

      if (!this.options.select) {
        option.id = 'f_' + this.options.id + '_' + idx;
        option.className = 'text submit button choice';
        option.observe('click', this._onSubmit.bindAsEventListener(this));
        this.form.appendChild(Element.textNode('   '));
      }
    }

    if (this.options.select) {
      this.form.appendChild(Element.textNode('   '));
      this.submit = $T('input', this.form, {type: 'submit',
        className: 'text submit', value: ' select '});
    }
  },

  _keyListHandler: function(ev) {
    if (ev.keyCode == Event.KEY_ESC) {
      this.close(ev);
      return false;
    }
    var el = Event.element(ev);
    if (el.hasClassName('choice')) {
      if (ev.keyCode == Event.KEY_RETURN) {
        this._onSubmit(ev);
      }
      var move = 0;
      if (ev.keyCode == Event.KEY_LEFT) move--;
      if (ev.keyCode == Event.KEY_RIGHT) move++;
      if (move != 0) {
        var crt = parseInt(el.id.replace(/^.+?_(\d+)$/, '$1'));
        var next = $( el.id.replace(/(\d+)$/, crt + move) );
        if (next) next.focus();
      }
    }
    return true;
  },

  _keySelectHandler: function(ev) {
    if (ev.keyCode == Event.KEY_ESC) {
      this.close(ev);
      return false;
    }
    if (ev.keyCode == Event.KEY_RETURN) {
      this._onSubmit(ev);
    }
    return true;
  },

  _onSubmit: function(ev) {
    var fld = this.options.select ? this.input : Event.element(ev);
    if (ev) Event.stop(ev);
    this.options.handler( fld.href || fld.value );
    this.destroy();
  }
});

Message.Progress = Class.create();
Message.Progress.prototype = Object.extend(new Message.Base(), {
  initialize: function(options) {
    Message.initialize();
    this.setOptions(Object.extend({
        barColors: [  '#FF0000', '#E3001C', '#C60039', '#AA0055', '#8E0071',
                      '#71008E', '#5500AA', '#3900C6', '#1C00E3', '#0000FF',
                      '#001AE6', '#0033CC', '#004DB3', '#006699', '#008080',
                      '#009966', '#00B34D', '#00CC33', '#00E61A', '#00FF00'
                  ],
        delay:       200,
        done:        0,
        getProgress: function() {return parseInt(this.meter.innerHTML) + parseInt(Math.random()*5+1)},
        onComplete:  function() {},
        level:       'progress',
        label:       'Progress',
        noClose:     true,
        maxDelay:    1000,
        minDelay:    100,
        monTimeout:  30,
        remove:      true,
        retry:       3,
        reverse:     false,
        stepDelay:   50,
        timeout:     0
      }, options || {}));
    if (this.options.reverse && !this.options.done)
      this.options.done = 100;
    if (this.options.reverse && (!options || !options.barColors))
      this.options.barColors = this.options.barColors.reverse();
    this.options.monTimeout *= 1000;
    this.getProgress = this.options.getProgress;
    this.zeros = this.stalled = 0;

    this.createProgress();
    return this.create(this.pr);
  },

  createProgress: function() {
    var id = 'msg-pr-' + this.options.id;
    this.pr = $T('div', {className: 'progress-wrapper', id: id});
    this.graph = $T('div', this.pr, {className: 'progress-graph', id: id + '-graph'});
    this.done = $T('div', this.graph, {className: 'progress-done', id: id + '-done'});
    this.meter = $T('div', this.pr, {className: 'progress-meter', id: id + '-meter'});
    this.label = $T('div', this.pr, {className: 'progress-label'}).update( 
      ' ' + (this.options.label || 'uploading') + '… ');
    this.update(this.options.done);
  },

  _onShow: function() {
    this.timer = setTimeout(this.monitor.bind(this), this.options.delay);
  },

  _onClose: function() {
    this.options.onComplete(this);
  },

  update: function(done) {
    var cidx = Math.round( done / Math.ceil(100 / this.options.barColors.length) );
    cidx = cidx < 0 ? 0 : cidx >= this.options.barColors.length
      ? this.options.barColors.length - 1 : cidx;
    this.meter.innerHTML = (done < 0 ? 0 : done) + '%';
    this.done.setStyle({backgroundColor: this.options.barColors[cidx],
      width: this.meter.innerHTML});
    return this;
  },

  hide: function (delay) {
    setTimeout(function() { this.close() }.bind(this), delay || this.options.delay);
  },

  completed: function () {
    this.update(this.options.reverse ? 0 : 100);
    this.label.innerHTML = this.label.innerHTML + ' <strong>DONE!</strong>';
    if (Message.animation)
      new Effect.Shake(this.msg);
    this.hide(3000);
  },

  failed: function () {
    this.pr.style.color = '#f00';
    this.update(this.options.reverse ? 100 : 0);
    if (Message.animation)
      new Effect.Shake(this.msg);
    this.hide(3000);
  },

  incDelay: function () {
    if (this.options.delay <= this.options.maxDelay - this.options.stepDelay)
      this.options.delay += this.options.stepDelay;
  },

  decDelay: function () {
    if (this.options.delay >= this.options.minDelay + this.options.stepDelay)
      this.options.delay -= this.options.stepDelay;
  },

  monitor: function () {
    if (this.timer) clearTimeout(this.timer);
    var current_proc = parseInt(this.meter.innerHTML);
    var proc = this.getProgress();
    if (proc == current_proc) {
      this.stalled += this.options.delay;
    } else {
      this.stalled = 0;
    }
    if (this.options.reverse) {
      if (proc >= 100) this.zeros++;
      if (current_proc > proc && proc > 0) {
        this.decDelay();
      } else {
        this.incDelay();
      }
    } else {
      if (proc <= 0) this.zeros++;
      if (current_proc < proc && proc < 100) {
        this.decDelay();
      } else {
        this.incDelay();
      }
    }
    this.update(proc);
    if ( this.zeros > this.options.retry || this.stalled > this.options.monTimeout)
      return this.failed();
    if ( proc >= 100 && !this.options.reverse )
      return this.completed();
    if ( proc <= 0 && this.options.reverse )
      return this.completed();
    if (proc < 100 && proc > 0) {
      this.timer = setTimeout(this.monitor.bind(this), this.options.delay);
    } else {
      this.failed();
    }
  }
});
